package org.dromara.common.log.aspect

import cn.hutool.core.lang.Dict
import cn.hutool.core.map.MapUtil
import cn.hutool.core.util.ArrayUtil
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.apache.commons.lang3.time.StopWatch
import org.aspectj.lang.JoinPoint
import org.aspectj.lang.annotation.AfterReturning
import org.aspectj.lang.annotation.AfterThrowing
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Before
import org.dromara.common.core.domain.model.LoginUser
import org.dromara.common.core.utils.ServletUtils.*
import org.dromara.common.core.utils.SpringUtils.context
import org.dromara.common.core.utils.StringUtils
import org.dromara.common.json.utils.JsonUtils.parseMap
import org.dromara.common.json.utils.JsonUtils.toJsonString
import org.dromara.common.log.annotation.Log
import org.dromara.common.log.enums.BusinessStatus
import org.dromara.common.log.event.OperLogEvent
import org.dromara.common.satoken.utils.LoginHelper
import org.dromara.common.satoken.utils.LoginHelper.getLoginUser
import org.springframework.boot.autoconfigure.AutoConfiguration
import org.springframework.http.HttpMethod
import org.springframework.validation.BindingResult
import org.springframework.web.multipart.MultipartFile
import java.util.*

/**
 * 操作日志记录处理
 *
 * @author Lion Li
 * @updater LikeYouDo
 * @date 2025/1/17 17:24
 */
@Aspect
@AutoConfiguration
class LogAspect {
    companion object {
        private val log = KotlinLogging.logger { }

        /**
         * 排除敏感属性字段
         */
        val EXCLUDE_PROPERTIES: Array<String> = arrayOf("password", "oldPassword", "newPassword", "confirmPassword")

        /**
         * 计时 key
         */
        private val KEY_CACHE: ThreadLocal<StopWatch> = ThreadLocal()
    }

    /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(controllerLog)")
    fun doBefore(joinPoint: JoinPoint?, controllerLog: Log?) {
        val stopWatch = StopWatch()
        KEY_CACHE.set(stopWatch)
        stopWatch.start()
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    fun doAfterReturning(joinPoint: JoinPoint, controllerLog: Log, jsonResult: Any?) {
        handleLog(joinPoint, controllerLog, null, jsonResult)
    }

    /**
     * 拦截异常操作
     *
     * @param joinPoint 切点
     * @param e         异常
     */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    fun doAfterThrowing(joinPoint: JoinPoint, controllerLog: Log, e: Exception?) {
        handleLog(joinPoint, controllerLog, e, null)
    }

    protected fun handleLog(joinPoint: JoinPoint, controllerLog: Log, e: Exception?, jsonResult: Any?) {
        try {
            // *========数据库日志=========*//
            val operLog = OperLogEvent().apply {
                tenantId = LoginHelper.getTenantId()
                status = BusinessStatus.SUCCESS.ordinal
                // 请求的地址
                val ip = getClientIP()
                operIp = ip
                operUrl = StringUtils.substring(getRequest().requestURI, 0, 255)
                val loginUser = getLoginUser<LoginUser>()
                operName = loginUser?.username
                deptName = loginUser?.deptName

                if (e != null) {
                    status = BusinessStatus.FAIL.ordinal
                    errorMsg = StringUtils.substring(e.message, 0, 3800)
                }

                // 设置方法名称
                val className = joinPoint.target.javaClass.name
                val methodName = joinPoint.signature.name
                method = "$className.$methodName()"
                // 设置请求方式
                requestMethod = getRequest().method
                // 处理设置注解上的参数
                getControllerMethodDescription(joinPoint, controllerLog, this, jsonResult)
                // 设置消耗时间
                val stopWatch = KEY_CACHE.get()
                stopWatch.stop()
                costTime = stopWatch.duration.toMillis()
            }

            // 发布事件保存数据库
            context().publishEvent(operLog)
        } catch (exp: Exception) {
            // 记录本地异常日志
            log.error { "异常信息:${exp.message}" }
        } finally {
            KEY_CACHE.remove()
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     *
     * @param log     日志
     * @param operLog 操作日志
     * @throws Exception
     */
    @Throws(Exception::class)
    fun getControllerMethodDescription(joinPoint: JoinPoint, log: Log, operLog: OperLogEvent, jsonResult: Any?) {
        // 设置action动作
        operLog.businessType = log.businessType.ordinal
        // 设置标题
        operLog.title = log.title
        // 设置操作人类别
        operLog.operatorType = log.operatorType.ordinal
        // 是否需要保存request，参数和值
        if (log.isSaveRequestData) {
            // 获取参数的信息，传入到数据库中。
            setRequestValue(joinPoint, operLog, log.excludeParamNames)
        }
        // 是否需要保存response，参数和值
        if (log.isSaveResponseData && jsonResult != null) {
            operLog.jsonResult =
                StringUtils.substring(toJsonString(jsonResult), 0, 3800)
        }
    }

    /**
     * 获取请求的参数，放到log中
     *
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    @Throws(Exception::class)
    private fun setRequestValue(joinPoint: JoinPoint, operLog: OperLogEvent, excludeParamNames: Array<String>) {
        val paramsMap: Map<String, String> = getParamMap(getRequest())
        val requestMethod = operLog.requestMethod
        if (MapUtil.isEmpty(paramsMap) && StringUtils.equalsAny(
                requestMethod,
                HttpMethod.PUT.name(),
                HttpMethod.POST.name(),
                HttpMethod.DELETE.name()
            )
        ) {
            val params = argsArrayToString(joinPoint.args, excludeParamNames)
            operLog.operParam = StringUtils.substring(params, 0, 3800)
        } else {
            MapUtil.removeAny(paramsMap, *EXCLUDE_PROPERTIES)
            MapUtil.removeAny(paramsMap, *excludeParamNames)
            operLog.operParam =
                StringUtils.substring(toJsonString(paramsMap), 0, 3800)
        }
    }

    /**
     * 参数拼装
     */
    private fun argsArrayToString(paramsArray: Array<Any?>, excludeParamNames: Array<String>): String {
        val params = StringJoiner(" ")
        if (ArrayUtil.isEmpty(paramsArray)) {
            return params.toString()
        }
        val exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES)
        for (o in paramsArray) {
            if (o != null && !isFilterObject(o)) {
                var str = ""
                if (o is MutableList<*>) {
                    val list1: MutableList<Dict?> = ArrayList()
                    for (obj in o) {
                        val str1 = toJsonString(obj)
                        val dict = parseMap(str1)
                        if (MapUtil.isNotEmpty(dict)) {
                            MapUtil.removeAny(dict, *exclude)
                            list1.add(dict)
                        }
                    }
                    str = toJsonString(list1)
                } else {
                    str = toJsonString(o)
                    val dict = parseMap(str)
                    if (!dict.isNullOrEmpty()) {
                        MapUtil.removeAny(dict, *exclude)
                        str = toJsonString(dict)
                    }
                }
                params.add(str)
            }
        }
        return params.toString()
    }

    /**
     * 判断是否需要过滤的对象。
     *
     * @param o 对象信息。
     * @return 如果是需要过滤的对象，则返回true；否则返回false。
     */
    fun isFilterObject(o: Any): Boolean {
        val clazz: Class<*> = o.javaClass
        if (clazz.isArray) {
            return MultipartFile::class.java.isAssignableFrom(clazz.componentType)
        } else if (MutableCollection::class.java.isAssignableFrom(clazz)) {
            val collection = o as Collection<*>
            for (value in collection) {
                return value is MultipartFile
            }
        } else if (MutableMap::class.java.isAssignableFrom(clazz)) {
            val map = o as Map<*, *>
            for (value in map.values) {
                return value is MultipartFile
            }
        }
        return o is MultipartFile || o is HttpServletRequest || o is HttpServletResponse
            || o is BindingResult
    }
}
